#pragma once


#include "math/Constants.hpp"
#include "math/Matrix.hpp"
#include "math/Random.hpp"
#include "trace/Ray.hpp"
#include <math.h>
#include <vector>


class Viewer
{
public:
    Viewer(): _rand(0.0f, 1.0f) {}
    ~Viewer() {}

    std::vector<Ray> RaysAtScreen(int screen_pixel_width, int screen_pixel_height, int screen_pixel_x, int screen_pixel_y, int num_samples)
    {
        /*
        ** axis vector of view space in world space
        */
        double phi = GetDirectionSphericalPhi();
        double theta = GetDirectionSphericalTheta();
        Vector z(-sin(theta) * cos(phi), -sin(theta) * sin(phi), -cos(theta));
        Vector x(Vector{0.0f, 0.0f, 1.0f}.cross(z));
        Vector y(z.cross(x));

        x.normalize();
        y.normalize();
        z.normalize();

        double screen_half_height{1.0f};
        double screen_half_width{GetAspectRatio()};

        std::vector<Ray> rays;
        for (int s = 0; s < num_samples; s++)
        {
            Vector direction{-z * (screen_half_height / tan(GetVerticalFOV() / 2.0f))};
            direction += x * (((screen_pixel_x /*+ _rand.next()*/) * 2 + 1 - screen_pixel_width) * screen_half_width / screen_pixel_width);
            direction -= y * (((screen_pixel_y /*+ _rand.next()*/) * 2 + 1 - screen_pixel_height) * screen_half_height / screen_pixel_height);
            direction.normalize();
            rays.emplace_back(GetLocation(), direction);
        }

        return rays;
    }

    void LookAt(const Vector& forward)
    {
        Vector forwardNormalized (forward.getNormalized()); 
        _theta = acos(forwardNormalized.z());
        _phi = acos(forwardNormalized.x() / sin(_theta));
        if (forwardNormalized.y() < 0)
            _phi = 2 * PI - _phi;
    }

    void SetLocation(const Point& loc)
    {
        _location = loc;
    }

    void RotateUp(double deltaAngle)
    {
        if (_theta - deltaAngle > 0)
            _theta -= deltaAngle;
    }

    void RotateDown(double deltaAngle)
    {
        if (_theta + deltaAngle < PI)
            _theta += deltaAngle;
    }

    void RotateLeft(double deltaAngle)
    {
        _phi += deltaAngle;
        _phi = fmod(_phi, PI * 2.0f);
    }

    void RotateRight(double deltaAngle)
    {
        _phi -= deltaAngle;
        _phi = fmod(_phi, PI * 2.0f);
    }

    void MoveForward(double step)
    {
        Vector forward_xy{sin(_theta) * cos(_phi), sin(_theta) * sin(_phi), 0.0f};
        forward_xy.normalize();
        _location += forward_xy * step;
    }

    void GoBack(double step)
    {
        Vector forward_xy{sin(_theta) * cos(_phi), sin(_theta) * sin(_phi), 0.0f};
        forward_xy.normalize();
        _location -= forward_xy * step;
    }

    void GoLeft(double step)
    {
        Vector forward_xy{sin(_theta) * cos(_phi), sin(_theta) * sin(_phi), 0.0f};
        forward_xy.normalize();
        _location += Vector(-forward_xy.y(), forward_xy.x(), 0.0f) * step;
    }

    void GoRight(double step)
    {
        Vector forward_xy{sin(_theta) * cos(_phi), sin(_theta) * sin(_phi), 0.0f};
        forward_xy.normalize();
        _location += Vector(forward_xy.y(), -forward_xy.x(), 0.0f) * step;
    }

    void setAspectRatio(double ratio)
    {
        _aspectRatio = abs(ratio);
    }

    void setVerticalFOV(double alpha)
    {
        _verticalFOV = alpha;
        if (_verticalFOV > PI) _verticalFOV = PI;
        if (_verticalFOV < 0.0f) _verticalFOV = 0.0f;
    }

    const Matrix& GetWorldToViewTransform() const
    {
        return _matWorldToView;
    }

    const Matrix& GetViewToWorldTransform() const
    {
        return _matViewToWorld;
    }

    const Matrix& GetProjectTransform() const
    {
        return _matProject;
    }

    const Matrix& GetWorldToNDCTransform() const
    {
        return _matWorldToNDC;
    }

    const Point& GetLocation() const
    {
        return _location;
    }

    double GetDirectionSphericalPhi() const
    {
        return _phi;
    }

    double GetDirectionSphericalTheta() const
    {
        return _theta;
    }

    double GetAspectRatio() const
    {
        return _aspectRatio;
    }

    double GetVerticalFOV() const
    {
        return _verticalFOV;
    }

private:
    void updateTransforms()
    {

        Vector z(-sin(_theta) * cos(_phi), -sin(_theta) * sin(_phi), -cos(_theta));
        Vector x(Vector{0.0f, 0.0f, 1.0f}.cross(z));
        Vector y(z.cross(x));

        x.normalize();
        y.normalize();
        z.normalize();

        Matrix P(x, y, z);
        Matrix P_inv(P.getTransposed());

        Point eye_transformed{P_inv * _location};
        _matWorldToView = Matrix{P_inv.data[0][0], P_inv.data[0][1], P_inv.data[0][2], -eye_transformed.x,
                                 P_inv.data[1][0], P_inv.data[1][1], P_inv.data[1][2], -eye_transformed.y,
                                 P_inv.data[2][0], P_inv.data[2][1], P_inv.data[2][2], -eye_transformed.z,
                                 0.0f,             0.0f,             0.0f,             1.0f};
        _matViewToWorld = _matWorldToView.Inverse();

        _matProject = Matrix{1.0f / (_aspectRatio * tan(_verticalFOV / 2.0f)), 0.0f,                            0.0f, 0.0f,
                             0.0f,                                             1.0f / tan(_verticalFOV / 2.0f), 0.0f, 0.0f,
                             0.0f,                                             0.0f,                            0.0f, 1.0f,
                             0.0f,                                             0.0f,                           -1.0f, 0.0f};

        _matWorldToNDC = _matProject * _matWorldToView;
    }

private:
    Point    _location;                 // viewer location in world space
    double   _theta = PI / 2;           // angle theta of view direction in spherical coordinate system
    double   _phi = 0.0f;               // angle phi   of view direction in spherical coordinate system
    double   _verticalFOV = PI / 2;     // field of view in vertical direction
    double   _aspectRatio = 1.6f;       // ratio of frustum width to height

    Matrix  _matWorldToView;            // world space to view space transform matrix
    Matrix  _matViewToWorld;            // view space to world space transform matrix
    Matrix  _matProject;                // projection matrix
    Matrix  _matWorldToNDC;             // world space to NDC space transform matrix

    RandomFloat _rand;
};